/*********************************************************************
*
*      Copyright (C) 2002 Andrew Khan
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
***************************************************************************/

package jxl.biff;

import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;

import jxl.common.Logger;

import jxl.WorkbookSettings;
import jxl.format.Format;
import jxl.read.biff.Record;

/**
 * A non-built in format record
 */
public class FormatRecord extends WritableRecordData implements DisplayFormat, Format {
	
	/**
	 * The logger
	 */
	public static Logger logger = Logger.getLogger(FormatRecord.class);

	/**
	 * Initialized flag
	 */
	private boolean initialized;

	/**
	 * The raw data
	 */
	private byte[] data;

	/**
	 * The index code
	 */
	private int indexCode;

	/**
	 * The formatting string
	 */
	private String formatString;

	/**
	 * Indicates whether this is a date formatting record
	 */
	private boolean date;

	/**
	 * Indicates whether this a number formatting record
	 */
	private boolean number;

	/**
	 * The format object
	 */
	private java.text.Format format;

	/**
	 * The date strings to look for
	 */
	private static String[] dateStrings = new String[] { "dd", "mm", "yy", "hh", "ss", "m/", "/d" };

	// Type to distinguish between biff7 and biff8
	private static class BiffType {
	}

	public static final BiffType biff8 = new BiffType();
	public static final BiffType biff7 = new BiffType();

	/**
	 * Constructor invoked when copying sheets
	 *
	 * @param fmt
	 *            the format string
	 * @param refno
	 *            the index code
	 */
	FormatRecord(String fmt, int refno) {
		super(Type.FORMAT);
		formatString = fmt;
		indexCode = refno;
		initialized = true;
	}

	/**
	 * Constructor used by writable formats
	 */
	protected FormatRecord() {
		super(Type.FORMAT);
		initialized = false;
	}

	/**
	 * Copy constructor - can be invoked by public access
	 *
	 * @param fr
	 *            the format to copy
	 */
	protected FormatRecord(FormatRecord fr) {
		super(Type.FORMAT);
		initialized = false;

		formatString = fr.formatString;
		date = fr.date;
		number = fr.number;
		// format = (java.text.Format) fr.format.clone();
	}

	/**
	 * Constructs this object from the raw data. Used when reading in a format
	 * record
	 *
	 * @param t
	 *            the raw data
	 * @param ws
	 *            the workbook settings
	 * @param biffType
	 *            biff type dummy overload
	 */
	public FormatRecord(Record t, WorkbookSettings ws, BiffType biffType) {
		super(t);

		byte[] data = getRecord().getData();
		indexCode = IntegerHelper.getInt(data[0], data[1]);
		initialized = true;

		if (biffType == biff8) {
			int numchars = IntegerHelper.getInt(data[2], data[3]);
			if (data[4] == 0) {
				formatString = StringHelper.getString(data, numchars, 5, ws);
			} else {
				formatString = StringHelper.getUnicodeString(data, numchars, 5);
			}
		} else {
			int numchars = data[2];
			byte[] chars = new byte[numchars];
			System.arraycopy(data, 3, chars, 0, chars.length);
			formatString = new String(chars);
		}

		date = false;
		number = false;

		// First see if this is a date format
		for (int i = 0; i < dateStrings.length; i++) {
			String dateString = dateStrings[i];
			if (formatString.indexOf(dateString) != -1 || formatString.indexOf(dateString.toUpperCase()) != -1) {
				date = true;
				break;
			}
		}

		// See if this is number format - look for the # or 0 characters
		if (!date) {
			if (formatString.indexOf('#') != -1 || formatString.indexOf('0') != -1) {
				number = true;
			}
		}
	}

	/**
	 * Used to get the data when writing out the format record
	 *
	 * @return the raw data
	 */
	public byte[] getData() {
		data = new byte[formatString.length() * 2 + 3 + 2];

		IntegerHelper.getTwoBytes(indexCode, data, 0);
		IntegerHelper.getTwoBytes(formatString.length(), data, 2);
		data[4] = (byte) 1; // unicode indicator
		StringHelper.getUnicodeBytes(formatString, data, 5);

		return data;
	}

	/**
	 * Gets the format index of this record
	 *
	 * @return the format index of this record
	 */
	public int getFormatIndex() {
		return indexCode;
	}

	/**
	 * Accessor to see whether this object is initialized or not.
	 *
	 * @return TRUE if this font record has been initialized, FALSE otherwise
	 */
	public boolean isInitialized() {
		return initialized;
	}

	/**
	 * Sets the index of this record. Called from the FormattingRecords object
	 *
	 * @param pos
	 *            the position of this font in the workbooks font list
	 */

	public void initialize(int pos) {
		indexCode = pos;
		initialized = true;
	}

	/**
	 * Replaces all instances of search with replace in the input. Used for
	 * replacing microsoft number formatting characters with java equivalents
	 *
	 * @param input
	 *            the format string
	 * @param search
	 *            the Excel character to be replaced
	 * @param replace
	 *            the java equivalent
	 * @return the input string with the specified substring replaced
	 */
	protected final String replace(String input, String search, String replace) {
		String fmtstr = input;
		int pos = fmtstr.indexOf(search);
		while (pos != -1) {
			StringBuffer tmp = new StringBuffer(fmtstr.substring(0, pos));
			tmp.append(replace);
			tmp.append(fmtstr.substring(pos + search.length()));
			fmtstr = tmp.toString();
			pos = fmtstr.indexOf(search);
		}
		return fmtstr;
	}

	/**
	 * Called by the immediate subclass to set the string once the Java-Excel
	 * replacements have been done
	 *
	 * @param s
	 *            the format string
	 */
	protected final void setFormatString(String s) {
		formatString = s;
	}

	/**
	 * Sees if this format is a date format
	 *
	 * @return TRUE if this format is a date
	 */
	public final boolean isDate() {
		return date;
	}

	/**
	 * Sees if this format is a number format
	 *
	 * @return TRUE if this format is a number
	 */
	public final boolean isNumber() {
		return number;
	}

	/**
	 * Gets the java equivalent number format for the formatString
	 *
	 * @return The java equivalent of the number format for this object
	 */
	public final NumberFormat getNumberFormat() {
		if (format != null && format instanceof NumberFormat) {
			return (NumberFormat) format;
		}

		try {
			String fs = formatString;

			// Replace the Excel formatting characters with java equivalents
			fs = replace(fs, "E+", "E");
			fs = replace(fs, "_)", "");
			fs = replace(fs, "_", "");
			fs = replace(fs, "[Red]", "");
			fs = replace(fs, "\\", "");

			format = new DecimalFormat(fs);
		} catch (IllegalArgumentException e) {
			// Something went wrong with the date format - fail silently
			// and return a default value
			format = new DecimalFormat("#.###");
		}

		return (NumberFormat) format;
	}

	/**
	 * Gets the java equivalent date format for the formatString
	 *
	 * @return The java equivalent of the date format for this object
	 */
	public final DateFormat getDateFormat() {
		if (format != null && format instanceof DateFormat) {
			return (DateFormat) format;
		}

		String fmt = formatString;

		// Replace the AM/PM indicator with an a
		int pos = fmt.indexOf("AM/PM");
		while (pos != -1) {
			StringBuffer sb = new StringBuffer(fmt.substring(0, pos));
			sb.append('a');
			sb.append(fmt.substring(pos + 5));
			fmt = sb.toString();
			pos = fmt.indexOf("AM/PM");
		}

		// Replace ss.0 with ss.SSS (necessary to always specify milliseconds
		// because of NT)
		pos = fmt.indexOf("ss.0");
		while (pos != -1) {
			StringBuffer sb = new StringBuffer(fmt.substring(0, pos));
			sb.append("ss.SSS");

			// Keep going until we run out of zeros
			pos += 4;
			while (pos < fmt.length() && fmt.charAt(pos) == '0') {
				pos++;
			}

			sb.append(fmt.substring(pos));
			fmt = sb.toString();
			pos = fmt.indexOf("ss.0");
		}

		// Filter out the backslashes
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < fmt.length(); i++) {
			if (fmt.charAt(i) != '\\') {
				sb.append(fmt.charAt(i));
			}
		}

		fmt = sb.toString();

		// If the date format starts with anything inside square brackets then
		// filter tham out
		if (fmt.charAt(0) == '[') {
			int end = fmt.indexOf(']');
			if (end != -1) {
				fmt = fmt.substring(end + 1);
			}
		}

		// Get rid of some spurious characters that can creep in
		fmt = replace(fmt, ";@", "");

		// We need to convert the month indicator m, to upper case when we
		// are dealing with dates
		char[] formatBytes = fmt.toCharArray();

		for (int i = 0; i < formatBytes.length; i++) {
			if (formatBytes[i] == 'm') {
				// Firstly, see if the preceding character is also an m. If so,
				// copy that
				if (i > 0 && (formatBytes[i - 1] == 'm' || formatBytes[i - 1] == 'M')) {
					formatBytes[i] = formatBytes[i - 1];
				} else {
					// There is no easy way out. We have to deduce whether this
					// an
					// minute or a month? See which is closest out of the
					// letters H d s or y
					// First, h
					int minuteDist = Integer.MAX_VALUE;
					for (int j = i - 1; j > 0; j--) {
						if (formatBytes[j] == 'h') {
							minuteDist = i - j;
							break;
						}
					}

					for (int j = i + 1; j < formatBytes.length; j++) {
						if (formatBytes[j] == 'h') {
							minuteDist = Math.min(minuteDist, j - i);
							break;
						}
					}

					for (int j = i - 1; j > 0; j--) {
						if (formatBytes[j] == 'H') {
							minuteDist = i - j;
							break;
						}
					}

					for (int j = i + 1; j < formatBytes.length; j++) {
						if (formatBytes[j] == 'H') {
							minuteDist = Math.min(minuteDist, j - i);
							break;
						}
					}

					// Now repeat for s
					for (int j = i - 1; j > 0; j--) {
						if (formatBytes[j] == 's') {
							minuteDist = Math.min(minuteDist, i - j);
							break;
						}
					}
					for (int j = i + 1; j < formatBytes.length; j++) {
						if (formatBytes[j] == 's') {
							minuteDist = Math.min(minuteDist, j - i);
							break;
						}
					}
					// We now have the distance of the closest character which
					// could
					// indicate the the m refers to a minute
					// Repeat for d and y
					int monthDist = Integer.MAX_VALUE;
					for (int j = i - 1; j > 0; j--) {
						if (formatBytes[j] == 'd') {
							monthDist = i - j;
							break;
						}
					}

					for (int j = i + 1; j < formatBytes.length; j++) {
						if (formatBytes[j] == 'd') {
							monthDist = Math.min(monthDist, j - i);
							break;
						}
					}
					// Now repeat for y
					for (int j = i - 1; j > 0; j--) {
						if (formatBytes[j] == 'y') {
							monthDist = Math.min(monthDist, i - j);
							break;
						}
					}
					for (int j = i + 1; j < formatBytes.length; j++) {
						if (formatBytes[j] == 'y') {
							monthDist = Math.min(monthDist, j - i);
							break;
						}
					}

					if (monthDist < minuteDist) {
						// The month indicator is closer, so convert to a
						// capital M
						formatBytes[i] = Character.toUpperCase(formatBytes[i]);
					} else if ((monthDist == minuteDist) && (monthDist != Integer.MAX_VALUE)) {
						// They are equidistant. As a tie-breaker, take the
						// formatting
						// character which precedes the m
						char ind = formatBytes[i - monthDist];
						if (ind == 'y' || ind == 'd') {
							// The preceding item indicates a month measure, so
							// convert
							formatBytes[i] = Character.toUpperCase(formatBytes[i]);
						}
					}
				}
			}
		}

		try {
			this.format = new SimpleDateFormat(new String(formatBytes));
		} catch (IllegalArgumentException e) {
			// There was a spurious character - fail silently
			this.format = new SimpleDateFormat("dd MM yyyy hh:mm:ss");
		}
		return (DateFormat) this.format;
	}

	/**
	 * Gets the index code, for use as a hash value
	 *
	 * @return the ifmt code for this cell
	 */
	public int getIndexCode() {
		return indexCode;
	}

	/**
	 * Gets the formatting string.
	 *
	 * @return the excel format string
	 */
	public String getFormatString() {
		return formatString;
	}

	/**
	 * Indicates whether this formula is a built in
	 *
	 * @return FALSE
	 */
	public boolean isBuiltIn() {
		return false;
	}

	/**
	 * Standard hash code method
	 * 
	 * @return the hash code value for this object
	 */
	public int hashCode() {
		return formatString.hashCode();
	}

	/**
	 * Standard equals method. This compares the contents of two format records,
	 * and not their indexCodes, which are ignored
	 *
	 * @param o
	 *            the object to compare
	 * @return TRUE if the two objects are equal, FALSE otherwise
	 */
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}

		if (!(o instanceof FormatRecord)) {
			return false;
		}

		FormatRecord fr = (FormatRecord) o;

		// Initialized format comparison
		if (initialized && fr.initialized) {
			// Must be either a number or a date format
			if (date != fr.date || number != fr.number) {
				return false;
			}

			return formatString.equals(fr.formatString);
		}

		// Uninitialized format comparison
		return formatString.equals(fr.formatString);
	}
}
